/*
 * Copyright (C) 2011-2015 René Jeschke <rene_jeschke@yahoo.de>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.rjeschke.txtmark;

import java.io.*;

/**
 * Markdown processor class.
 *
 * <p>
 * Example usage:
 * </p>
 *
 * <pre>
 * <code>String result = Processor.process("This is ***TXTMARK***");
 * </code>
 * </pre>
 *
 * @author René Jeschke &lt;rene_jeschke@yahoo.de&gt;
 */
public class Processor {
    /**
     * The reader.
     */
    private final Reader reader;
    /**
     * The emitter.
     */
    private final Emitter emitter;
    /**
     * The Configuration.
     */
    final Configuration config;
    /**
     * Extension flag.
     */
    private boolean useExtensions = false;

    /**
     * Constructor.
     *
     * @param reader The input reader.
     */
    private Processor(final Reader reader, final Configuration config) {
        this.reader = reader;
        this.config = config;
        this.useExtensions = config.forceExtendedProfile;
        this.emitter = new Emitter(this.config);
    }

    /**
     * Transforms an input stream into HTML using the given Configuration.
     *
     * @param reader        The Reader to process.
     * @param configuration The Configuration.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration
     * @since 0.7
     */
    public final static String process(final Reader reader, final Configuration configuration) throws IOException {
        final Processor p = new Processor(!(reader instanceof BufferedReader) ? new BufferedReader(reader) : reader,
                configuration);
        return p.process();
    }

    /**
     * Transforms an input String into HTML using the given Configuration.
     *
     * @param input         The String to process.
     * @param configuration The Configuration.
     * @return The processed String.
     * @see Configuration
     * @since 0.7
     */
    public final static String process(final String input, final Configuration configuration) {
        try {
            return process(new StringReader(input), configuration);
        } catch (final IOException e) {
            // This _can never_ happen
            return null;
        }
    }

    /**
     * Transforms an input file into HTML using the given Configuration.
     *
     * @param file          The File to process.
     * @param configuration the Configuration
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration
     * @since 0.7
     */
    public final static String process(final File file, final Configuration configuration) throws IOException {
        final FileInputStream input = new FileInputStream(file);
        final String ret = process(input, configuration);
        input.close();
        return ret;
    }

    /**
     * Transforms an input stream into HTML using the given Configuration.
     *
     * @param input         The InputStream to process.
     * @param configuration The Configuration.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration
     * @since 0.7
     */
    public final static String process(final InputStream input, final Configuration configuration) throws IOException {
        final Processor p = new Processor(new BufferedReader(new InputStreamReader(input, configuration.encoding)),
                configuration);
        return p.process();
    }

    /**
     * Transforms an input String into HTML using the default Configuration.
     *
     * @param input The String to process.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input) {
        return process(input, Configuration.DEFAULT);
    }

    /**
     * Transforms an input String into HTML.
     *
     * @param input    The String to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input, final boolean safeMode) {
        return process(input, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input String into HTML.
     *
     * @param input     The String to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input, final Decorator decorator) {
        return process(input, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input String into HTML.
     *
     * @param input     The String to process.
     * @param decorator The decorator to use.
     * @param safeMode  Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input, final Decorator decorator, final boolean safeMode) {
        return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML using the default Configuration.
     *
     * @param file The File to process.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file) throws IOException {
        return process(file, Configuration.DEFAULT);
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file     The File to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final boolean safeMode) throws IOException {
        return process(file, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file      The File to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final Decorator decorator) throws IOException {
        return process(file, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file      The File to process.
     * @param decorator The decorator to use.
     * @param safeMode  Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final Decorator decorator, final boolean safeMode)
            throws IOException {
        return process(file, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file     The File to process.
     * @param encoding The encoding to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding) throws IOException {
        return process(file, Configuration.builder().setEncoding(encoding).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file     The File to process.
     * @param encoding The encoding to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding, final boolean safeMode)
            throws IOException {
        return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file      The File to process.
     * @param encoding  The encoding to use.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding, final Decorator decorator)
            throws IOException {
        return process(file, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file      The File to process.
     * @param encoding  The encoding to use.
     * @param decorator The decorator to use.
     * @param safeMode  Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding, final Decorator decorator,
                                       final boolean safeMode) throws IOException {
        return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode)
                .setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input) throws IOException {
        return process(input, Configuration.DEFAULT);
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input    The InputStream to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final boolean safeMode) throws IOException {
        return process(input, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input     The InputStream to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final Decorator decorator) throws IOException {
        return process(input, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input     The InputStream to process.
     * @param decorator The decorator to use.
     * @param safeMode  Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final Decorator decorator, final boolean safeMode)
            throws IOException {
        return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input    The InputStream to process.
     * @param encoding The encoding to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding) throws IOException {
        return process(input, Configuration.builder().setEncoding(encoding).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input    The InputStream to process.
     * @param encoding The encoding to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding, final boolean safeMode)
            throws IOException {
        return process(input, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input     The InputStream to process.
     * @param encoding  The encoding to use.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding, final Decorator decorator)
            throws IOException {
        return process(input, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input     The InputStream to process.
     * @param encoding  The encoding to use.
     * @param decorator The decorator to use.
     * @param safeMode  Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding, final Decorator decorator,
                                       final boolean safeMode) throws IOException {
        return process(input,
                Configuration.builder().setEncoding(encoding).setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML using the default Configuration.
     *
     * @param reader The Reader to process.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader) throws IOException {
        return process(reader, Configuration.DEFAULT);
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param reader   The Reader to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader, final boolean safeMode) throws IOException {
        return process(reader, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param reader    The Reader to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader, final Decorator decorator) throws IOException {
        return process(reader, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param reader    The Reader to process.
     * @param decorator The decorator to use.
     * @param safeMode  Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader, final Decorator decorator, final boolean safeMode)
            throws IOException {
        return process(reader, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Reads all lines from our reader.
     * <p>
     * Takes care of markdown link references.
     * </p>
     *
     * @return A Block containing all lines.
     * @throws IOException If an IO error occurred.
     */
    private Block readLines() throws IOException {
        final Block block = new Block();
        final StringBuilder sb = new StringBuilder(80);
        int c = this.reader.read();
        LinkRef lastLinkRef = null;
        while (c != -1) {
            sb.setLength(0);
            int pos = 0;
            boolean eol = false;
            while (!eol) {
                switch (c) {
                    case -1:
                        eol = true;
                        break;
                    case '\n':
                        c = this.reader.read();
                        if (c == '\r') {
                            c = this.reader.read();
                        }
                        eol = true;
                        break;
                    case '\r':
                        c = this.reader.read();
                        if (c == '\n') {
                            c = this.reader.read();
                        }
                        eol = true;
                        break;
                    case '\t': {
                        final int np = pos + (4 - (pos & 3));
                        while (pos < np) {
                            sb.append(' ');
                            pos++;
                        }
                        c = this.reader.read();
                        break;
                    }
                    default:
                        if (c != '<' || !this.config.panicMode) {
                            pos++;
                            sb.append((char) c);
                        } else {
                            pos += 4;
                            sb.append("&lt;");
                        }
                        c = this.reader.read();
                        break;
                }
            }

            final Line line = new Line();
            line.value = sb.toString();
            line.init();

            // Check for link definitions
            boolean isLinkRef = false;
            String id = null, link = null, comment = null;
            if (!line.isEmpty && line.leading < 4 && line.value.charAt(line.leading) == '[') {
                line.pos = line.leading + 1;
                // Read ID up to ']'
                id = line.readUntil(']');
                // Is ID valid and are there any more characters?
                if (id != null && line.pos + 2 < line.value.length()) {
                    // Check for ':' ([...]:...)
                    if (line.value.charAt(line.pos + 1) == ':') {
                        line.pos += 2;
                        line.skipSpaces();
                        // Check for link syntax
                        if (line.value.charAt(line.pos) == '<') {
                            line.pos++;
                            link = line.readUntil('>');
                            line.pos++;
                        } else {
                            link = line.readUntil(' ', '\n');
                        }

                        // Is link valid?
                        if (link != null) {
                            // Any non-whitespace characters following?
                            if (line.skipSpaces()) {
                                final char ch = line.value.charAt(line.pos);
                                // Read comment
                                if (ch == '\"' || ch == '\'' || ch == '(') {
                                    line.pos++;
                                    comment = line.readUntil(ch == '(' ? ')' : ch);
                                    // Valid linkRef only if comment is valid
                                    if (comment != null) {
                                        isLinkRef = true;
                                    }
                                }
                            } else {
                                isLinkRef = true;
                            }
                        }
                    }
                }
            }

            // To make compiler happy: add != null checks
            if (isLinkRef && id != null && link != null) {
                if (id.toLowerCase().equals("$profile$")) {
                    this.emitter.useExtensions = this.useExtensions = link.toLowerCase().equals("extended");
                    lastLinkRef = null;
                } else {
                    // Store linkRef and skip line
                    final LinkRef lr = new LinkRef(link, comment, comment != null
                            && (link.length() == 1 && link.charAt(0) == '*'));
                    this.emitter.addLinkRef(id, lr);
                    if (comment == null) {
                        lastLinkRef = lr;
                    }
                }
            } else {
                comment = null;
                // Check for multi-line linkRef
                if (!line.isEmpty && lastLinkRef != null) {
                    line.pos = line.leading;
                    final char ch = line.value.charAt(line.pos);
                    if (ch == '\"' || ch == '\'' || ch == '(') {
                        line.pos++;
                        comment = line.readUntil(ch == '(' ? ')' : ch);
                    }
                    if (comment != null) {
                        lastLinkRef.title = comment;
                    }

                    lastLinkRef = null;
                }

                // No multi-line linkRef, store line
                if (comment == null) {
                    line.pos = 0;
                    block.appendLine(line);
                }
            }
        }

        return block;
    }

    /**
     * Initializes a list block by separating it into list item blocks.
     *
     * @param root The Block to process.
     */
    private void initListBlock(final Block root) {
        Line line = root.lines;
        line = line.next;
        while (line != null) {
            final LineType t = line.getLineType(this.config);
            if ((t == LineType.OLIST || t == LineType.ULIST)
                    || (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST)))) {
                root.split(line.previous).type = BlockType.LIST_ITEM;
            }
            line = line.next;
        }
        root.split(root.lineTail).type = BlockType.LIST_ITEM;
    }

    /**
     * Recursively process the given Block.
     *
     * @param root     The Block to process.
     * @param listMode Flag indicating that we're in a list item block.
     */
    private void recurse(final Block root, final boolean listMode) {
        Block block, list;
        Line line = root.lines;

        if (listMode) {
            root.removeListIndent(this.config);
            if (this.useExtensions && root.lines != null && root.lines.getLineType(this.config) != LineType.CODE) {
                root.id = root.lines.stripID();
            }
        }

        while (line != null && line.isEmpty) {
            line = line.next;
        }
        if (line == null) {
            return;
        }

        while (line != null) {
            final LineType type = line.getLineType(this.config);
            switch (type) {
                case OTHER: {
                    final boolean wasEmpty = line.prevEmpty;
                    while (line != null && !line.isEmpty) {
                        final LineType t = line.getLineType(this.config);
                        if ((listMode || this.useExtensions) && (t == LineType.OLIST || t == LineType.ULIST)) {
                            break;
                        }
                        if (this.useExtensions && (t == LineType.CODE || t == LineType.FENCED_CODE)) {
                            break;
                        }
                        if (t == LineType.HEADLINE || t == LineType.HEADLINE1 || t == LineType.HEADLINE2
                                || t == LineType.HR
                                || t == LineType.BQUOTE || t == LineType.XML) {
                            break;
                        }
                        line = line.next;
                    }
                    final BlockType bt;
                    if (line != null && !line.isEmpty) {
                        bt = (listMode && !wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH;
                        root.split(line.previous).type = bt;
                        root.removeLeadingEmptyLines();
                    } else {
                        bt = (listMode && (line == null || !line.isEmpty) && !wasEmpty) ? BlockType.NONE
                                : BlockType.PARAGRAPH;
                        root.split(line == null ? root.lineTail : line).type = bt;
                        root.removeLeadingEmptyLines();
                    }
                    line = root.lines;
                    break;
                }
                case CODE:
                    while (line != null && (line.isEmpty || line.leading > 3)) {
                        line = line.next;
                    }
                    block = root.split(line != null ? line.previous : root.lineTail);
                    block.type = BlockType.CODE;
                    block.removeSurroundingEmptyLines();
                    break;
                case XML:
                    if (line.previous != null) {
                        // FIXME ... this looks wrong
                        root.split(line.previous);
                    }
                    root.split(line.xmlEndLine).type = BlockType.XML;
                    root.removeLeadingEmptyLines();
                    line = root.lines;
                    break;
                case BQUOTE:
                    while (line != null) {
                        if (!line.isEmpty
                                && (line.prevEmpty && line.leading == 0 && line.getLineType(this.config) != LineType.BQUOTE)) {
                            break;
                        }
                        line = line.next;
                    }
                    block = root.split(line != null ? line.previous : root.lineTail);
                    block.type = BlockType.BLOCKQUOTE;
                    block.removeSurroundingEmptyLines();
                    block.removeBlockQuotePrefix();
                    this.recurse(block, false);
                    line = root.lines;
                    break;
                case HR:
                    if (line.previous != null) {
                        // FIXME ... this looks wrong
                        root.split(line.previous);
                    }
                    root.split(line).type = BlockType.RULER;
                    root.removeLeadingEmptyLines();
                    line = root.lines;
                    break;
                case FENCED_CODE:
                    line = line.next;
                    while (line != null) {
                        if (line.getLineType(this.config) == LineType.FENCED_CODE) {
                            break;
                        }
                        // TODO ... is this really necessary? Maybe add a special
                        // flag?
                        line = line.next;
                    }
                    if (line != null) {
                        line = line.next;
                    }
                    block = root.split(line != null ? line.previous : root.lineTail);
                    block.type = BlockType.FENCED_CODE;
                    block.meta = Utils.getMetaFromFence(block.lines.value);
                    block.lines.setEmpty();
                    if (block.lineTail.getLineType(this.config) == LineType.FENCED_CODE) {
                        block.lineTail.setEmpty();
                    }
                    block.removeSurroundingEmptyLines();
                    break;
                case HEADLINE:
                case HEADLINE1:
                case HEADLINE2:
                    if (line.previous != null) {
                        root.split(line.previous);
                    }
                    if (type != LineType.HEADLINE) {
                        line.next.setEmpty();
                    }
                    block = root.split(line);
                    block.type = BlockType.HEADLINE;
                    if (type != LineType.HEADLINE) {
                        block.hlDepth = type == LineType.HEADLINE1 ? 1 : 2;
                    }
                    if (this.useExtensions) {
                        block.id = block.lines.stripID();
                    }
                    block.transfromHeadline();
                    root.removeLeadingEmptyLines();
                    line = root.lines;
                    break;
                case OLIST:
                case ULIST:
                    while (line != null) {
                        final LineType t = line.getLineType(this.config);
                        if (!line.isEmpty
                                && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST))) {
                            break;
                        }
                        line = line.next;
                    }
                    list = root.split(line != null ? line.previous : root.lineTail);
                    list.type = type == LineType.OLIST ? BlockType.ORDERED_LIST : BlockType.UNORDERED_LIST;
                    list.lines.prevEmpty = false;
                    list.lineTail.nextEmpty = false;
                    list.removeSurroundingEmptyLines();
                    list.lines.prevEmpty = list.lineTail.nextEmpty = false;
                    this.initListBlock(list);
                    block = list.blocks;
                    while (block != null) {
                        this.recurse(block, true);
                        block = block.next;
                    }
                    list.expandListParagraphs();
                    break;
                default:
                    line = line.next;
                    break;
            }
        }
    }

    /**
     * Does all the processing.
     *
     * @return The processed String.
     * @throws IOException If an IO error occurred.
     */
    private String process() throws IOException {
        final StringBuilder out = new StringBuilder();
        final Block parent = this.readLines();
        parent.removeSurroundingEmptyLines();

        this.recurse(parent, false);
        Block block = parent.blocks;
        while (block != null) {
            this.emitter.emit(out, block);
            block = block.next;
        }

        return out.toString();
    }
}
